<?php
// {{{ class ConnectDB
/**
 * ConnectDB for MySQL
 * ConnectDBのMySQL動作群
 */
class ConnectDB_MySQL {

	/**
	 * PDOのインスタンス
	 */	
	public $PDO;
	
	/**
	 * PDOStatementのインスタンス
	 */
	private $PDOStatement;

	/**
	 * 直前に実行したクエリ
	 */
	private $prevQuery;

	/**
	 * 各種キャッシュ
	 */
	private $Cache;

	/**
	 * 接続状態
	 */
	private $DataBase;			// 現在選択中のデータベース
	private $Transaction;		// トランザクションのフラグ

	public function __construct () {
		$this->DataBase = null;
		$this->Transaction = false;
	}
	
	/**
	 * init
	 */
	function init () {
		Y('this.Result+', YuTori::Create($this, 'getResult'));
		Y('this.ResultAll+', YuTori::Create($this, 'getResultAll'));

		Y('this.beginTransaction+', YuTori::Create($this, 'beginTransaction'));
		Y('this.endTransaction+', YuTori::Create($this, 'endTransaction'));
		Y('this.rollbackTransaction+', YuTori::Create($this, 'rollbackTransaction'));

		Y('this.Result+', YuTori::Create($this, 'getResult'));
		Y('this.PDO+', null);
	}

	/**
	 * call
	 */
	public function call ($Query, $Data = array()) {
		if (is_string($Query)) {
			return $this->execQuery($Query, $Data);
		} else {
			$Args = new ConnectDB_MySQL_Query ($Query, $Data);
			if (!$Args->query) throw new YuToriException ('クエリが指定されていません。');
			if (!method_exists($this, $Args->query)) throw new YuToriException ('ConnectDB:不正なクエリを検出しました。/'.$Args->query);
			$Method = $Args->query;
			return $this->$Method ($Args);
		}
	}

	/**
	 * SQLサーバに接続する。
	 * キー名	:	解説（※は必須）
	 * dsn		:	※使用するSQLサーバのDSN文字列
	 * host		:	※使用するSQLサーバのホスト名
	 * dbname	:	※使用するデータベース名
	 * user		:	※使用するSQLサーバのユーザ名
	 * pass		:	※使用するSQLサーバのパスワード
	 * 
	 * @param
	 * @return boolean 問題がなければtrue
	 * @access public
	 */
	function Connect () {
		if ($this->isconnect()) return false;

		// キャッシュクリア&ロード
		$this->Cache = null;
		Y('Module.load', 'Vars');
		$this->Cache = Y('Module.List.Vars.sys.Cache', 'ConnectDB');
		
		try {
			$this->PDO = new pdo (Y('App.Helper.ConnectDB.DSN').':host='.Y('App.Helper.ConnectDB.Host').';dbname='.Y('App.Helper.ConnectDB.DBName')
			, Y('App.Helper.ConnectDB.User'), Y('App.Helper.ConnectDB.Pass'));

			$this->PDO->setAttribute (PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
			$this->PDO->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, true);
			$this->DataBase = Y('App.Helper.ConnectDB.DBName');

			$this->setMBEncode();
			
			return true;
		} catch (PDOEXception $ex) {
			throw new YuToriException ('DB接続に失敗しました。', 0, $ex);
			return false;
		}
	}

	/**
	 * DB接続を解除する。
	 */
	public function disconnect () {
		if (!$this->isconnect()) return false;
		$YuTori = getSystem('YuTori');

		try {
			if ($YuTori->getConfig('DebugMode','switch')) {
				$res = $this->PDO->query ('SHOW STATUS WHERE 	Variable_name LIKE "Com\_%" AND Value > 0');
				$Log = getSystem('LogManager');
				while ($Result = $res->fetch ()) {
					$Log->setDebugLog ($Result['Variable_name']. ' = '. $Result['Value']);
				}
			}
			$this->PDO = null;
			$this->DataBase = null;
		} catch (PDOEXception $ex) {
			throw new YuToriException ('DB接続解除に失敗しました。', 0, $ex);
			return false;
		}
		return true;
	}

	/**
	 * 現在の接続状態を調べる。
	 */
	public function isconnect () {
		return $this->DataBase;
	}

	/**
	 * 文字コードを設定する
	 * 
	 * @param string $Code 文字コード名。空にするとデフォルトになります。(sjis、ujis、utf8)
	 * @return boolean 				問題がなければtrue
	 * @access public
	 */
	public function setMBEncode ($Code = null) {
		if (!$this->isconnect()) $this->connect();
		if ($Code == null) $Code = Y('App.Helper.ConnectDB.MBEncode');
		return $this->execQuery ('SET NAMES "'.$Code.'"', array ());
	}

	/**
	 * クエリを実行する
	 * 
	 * @param string $Query 実行するクエリ
	 * @return クエリの実行結果
	 * @access public
	 */
	public function execQuery ($Query, $Data = array(), &$rowCount = null) {
		if (!$this->isconnect()) $this->connect();
		try {
			$this->PDOStatement = $this->PDO->prepare ($Query);

			$this->PDOStatement->setFetchMode (PDO::FETCH_ASSOC);
			$Res = $this->PDOStatement->execute($Data);
			$this->prevQuery = $Query;
			if ($rowCount) $rowCount = $this->PDOStatement->rowCount();
			if (Y('Logger.Debug$')) {
				$Log[] = 'クエリ実行 '.$Query;
				foreach ((array)$Data as $key=>$val) {
					$Log[] = ':'.$key .'="'. $val . '" ';
				}
				$Log[] = 'rowCount='. $this->PDOStatement->rowCount();
				Y('Logger.Debug', $Log);
			}
			return $Res;
		} catch (PDOEXception $ex) {
			$Msg = array ();
			$Msg[] = 'クエリエラー:'. $Query;
			$Msg[] = $ex->getMessage();
			foreach ($Data as $key=>$val) {
				$Msg[] = 'DATA: $'. $key. ' = '. $val;
			}
			throw new YuToriException ($Msg, 0);
			return false;
		}
	}

	/**
	 * テーブルのカラム構造を取得する
	 * auto_increment指定してあるカラムは取得しない
	 * 
	 * @param string $Table 構造を取得するテーブル名。
	 * @param bool $UseCache falseを指定するとキャッシュを使用せず新たに取得します。
	 * @return array 取得したカラム構造
	 * @access public
	 */
	public function getColumnsFromTable ($Table, $UseCache = true) {
		if (!$this->isconnect()) $this->connect();
		$Table = $this->validateTable($Table);
		if ($UseCache && !empty ($this->Cache['Columns'][$Table])) return $this->Cache['Columns'][$Table];
		$this->execQuery('SHOW COLUMNS FROM '.$Table);
		$Columns = $this->getResultAll();
		if (empty($Columns)) return false;
		// 取得したテーブルデータの整形
		$Return = array ();
		foreach ($Columns as $val) {
			$Field = $val['Field'];
			unset ($val['Field']);
			$Return[$Field] = $val;
		}
		$this->Cache['Columns'][$Table] = $Return;

		Y('Module.List.Vars.sys.Cache', 'ConnectDB', 'Columns', $this->Cache['Columns']);

		return $Return;
	}

	/**
	 * WHERE文の発行
	 * `配列の値`=:配列の値 というWHERE文を発行します。
	 * 配列が連想配列の場合、キー名を使用します。
	 * 全て$Separaorに指定した文字列(AND/OR)で区切ります。
	 * $Operatorに指定した演算子を利用します。
	 * 
	 * 注意：SQLインジェクションとなりえるため、$Separatorと$Operatorに
	 * 　　　ユーザからの入力値を使用しないで下さい。
	 * 
	 * @param array $WhereArray WHEREとして発行する変数。'カラム名'=>'データ'
	 * @param string $Separator ANDもしくはOR。指定がなければANDとなります。
	 * @param string $Operator 使用する演算子。指定がなければ=となります。
	 * @return 作成したWHERE文。'WHERE'そのものは含まれません。
	 * @access public
	 */
	public function createSQL_Where ($WhereArray, $Separator ='AND', $Operator = '=') {
		if (!is_array ($WhereArray)) return false;
		$Where = array ();
		foreach ($WhereArray as $akey=>$aval) {
			// 配列が連想配列かどうか調べる
			if (is_numeric ($akey)) {
				$key = $aval;
			} else {
				$key = $akey;
			}
			
			$Where[] = '`'.$key.'` '.$Operator.' :'.$key;
		}
		$Where = implode ( $Where, ' '.$Separator.' ');
		
		if (empty($Where)) $Where = '1';
		
		return $Where;
	}

	/**
	 * オプションクエリを生成する
	 * 
	 * @param array $Options オプションの配列
	 * @return array 取得したレコード
	 * @access public
	 */
	public function createSQL_Option ($Options) {

		$Query = '';
		if ($Options->group) {
			$Query .= ' GROUP BY '. $this->validateColumn($Options->group);
		}
		if ($Options->order) {
			$Query .= ' ORDER BY '. $this->validateColumn($Options->order);
		}
		if ($Options->limit) {
			if ((int)$Options->limit > 0) {
				$Query .= ' LIMIT '. (int)$Options->limit;
			}
			if ($Options->offset) {
				$Query .= ' OFFSET '. (int)$Options->offset;
			}
		}

		return $Query;
	}

	/**
	 * 実行したクエリから情報を取得する
	 * 
	 * @return mixed PDOStatementが返す戻り値
	 * @access public
	 */
	public function getResult () {
		try {
			if (empty($this->PDOStatement)) return false;
			$Res = $this->PDOStatement->fetch();
			return $Res;
		} catch (PDOEXception $ex) {
			throw new YuToriException ('結果取得に失敗しました。'."\n".$ex->getMessage(), 0);
		}
	}

	/**
	 * 実行したクエリから全ての情報を取得する
	 * 
	 * @return mixed PDOStatementが返す戻り値
	 * @access public
	 */
	public function getResultAll () {
		try {
			if (empty($this->PDOStatement)) return false;
			$Res = $this->PDOStatement->fetchAll();
			return $Res;
		} catch (PDOEXception $ex) {
			throw new YuToriException ('結果取得に失敗しました。'."\n".$ex->getMessage(), 0);
		}
	}

	/**
	 * テーブルバリデータ
	 * テーブル名を整形する
	 * JOIN対応版(現状、結合処理を行うとデータバリデータが利用できません)
	 * 
	 * $Tableに配列を指定すると、すべてJOINで結合されます。
	 * キー0が起点となります。
	 * 添字にCROSS、INNER、LEFT、RIGHTを使用することで、結合種別が指定できます。
	 * 指定なしの場合、CROSSになります。
	 * ON句は添字ONに指定してください。
	 * (制約：同一の結合種別は１つまでしか使用できません)
	 * 
	 * @param mixed $Table 整形するテーブル。DB名.テーブル名という指定も可。
	 * @return array バリデートしたテーブル名
	 * @access public
	 */
	public function validateTable ($Table) {
		if (is_array($Table)) {
			$On = '';
			$Return = '';
			foreach ($Table as $key=>$val) {
				if (is_numeric($key)) {
					if ($key == 0) {
						$Return .= $this->validateTable($val);
						continue;
					} else {
						$key = 'CROSS';
					}
				}

				$key = strtoupper ($key);
				if ($key == 'ON') {
					$On = ' ON '.$val;
				} else {
					$Return .= ' '.$key. ' JOIN '.$this->validateTable($val);
				}
			}
			return $Return. $On;
		} else {
			$Return = trim($Table);
			$Return = str_replace ('`', '', $Return);
			
			$Return = explode (' ', $Return, 2);
			if (isset($Return[1])) $Add = ' '.$Return[1];
			else $Add = '';
			$Return = strtolower($Return[0]);

			if (strpos($Return,'.') !== false) {
				$Return = explode ('.', $Return, 2);
				// 配列の整形
				$Return[0] = str_replace ('.', '', $Return[0]);
				if (!isset ($Return[1])) $Return[1]='';
				$Return[0] = trim ($Return[0]); $Return[1] = trim ($Return[1]);
	
				// 両方空のときはfalse
				if (!($Return[0] || $Return[1])) return false;
	
				if (strlen($Return[1])) {
					if (strlen($Return[0])) $Return[0] = '`'.$Return[0].'`.';
					return strtolower($Return[0]. '`'.$Return[1].'`');
				} else {
					if (strlen($Return[0])) $Return[0] = '`'.$Return[0].'`.';
					return $Return[0];
				} 
			} else {
				return '`'.$Return.'`'. $Add;
			}
		}
	}

	/**
	 * データバリデータ
	 * テーブルに存在しない変数を除外してプレイスホルダにバインドする
	 * 
	 * @param string $Table 参照するテーブル
	 * @param array $Data バリデートする配列
	 * @return array バリデートした配列
	 * @access public
	 */
	public function validateData ($Table, $Data) {
		if (is_array($Table)) return $Data;

		$Columns = $this->getColumnsFromTable($Table);
		$Return = array ();
		foreach ($Data as $key=>$val) {
			if (isset ($Columns[$key])) $Return[$key] = $val;
		}
		return $Return;
	}

	/**
	 * カラムバリデータ
	 * カラム名を整形する
	 * 
	 * @param string $Column 整形するカラム。,区切りで複数指定可能
	 * @return array バリデートしたテーブル名
	 * @access public
	 */
	public function validateColumn ($Column) {
		$Return = '';
		$Column2 = $Column;
		$Column2 = str_replace ('`', '', $Column2);
		$Column2 = explode (',', $Column2);
		foreach ($Column2 as $key=>$val) {
			$val = trim($val);
			if ($val == '*') continue;
			
			// DESC ASCの検索
			if (strpos($val, ' DESC')) {
				$val = explode (' ', $val, 2);
				$Column2[$key] = '`'.trim($val[0]).'` DESC';
			}
		}
		$Return = implode ($Column2, ',');

		return $Return;
	}

	/**
	 * トランザクションを開始する
	 */
	public function beginTransaction() {
		if ($this->Transaction) return true;
		if (!$this->isconnect()) $this->connect();
		Y('Logger.Debug', 'トランザクションを開始');
		$this->Transaction = true;
		return $this->PDO->beginTransaction();
	}

	/**
	 * トランザクションを終了する
	 */
	public function endTransaction() {
		if (!$this->Transaction) return true;
		Y('Logger.Debug', 'トランザクションを終了');
		return $this->PDO->commit();
	}

	/**
	 * トランザクションをロールバックする
	 */
	public function rollbackTransaction() {
		if (!$this->Transaction) return true;
		Y('Logger.Debug', 'トランザクションをロールバック');
		return $this->PDO->rollBack();
	}

	//---------------------------以下クエリ--------------------------
	/**
	 * SELECTクエリを発行する
	 * 
	 * 引数$Optionsで動作の設定ができます。
	 * キー名	:	解説
	 * limit	:	LIMITの値
	 * offset	:	OFFSETの値
	 * group	:	GROUP BYの値
	 * order	:	ORDER BYの値
	 * 
	 * @param string $Table 参照するテーブル
	 * @param string $WhereQuery WHERE文。指定がなければ$Dataから自動で生成されます。
	 * @param string $Data プレイスホルダにバインドする配列
	 * @param string $Column 取得するカラム(初期値*)
	 * @return mixed PDOStatementが返す戻り値
	 * @access public
	 */
	public function Select ($Query) {
		if ($Query->autovalidate) {
			$Table = $this->validateTable($Query->table);
			$Column = $this->validateColumn($Query->column);
		} else {
			$Table = $Query->table;
			$Column = $Query->column;
		}

		if (!$Query->where) {
			if ($Query->autovalidate) $Query->data = $this->validateData($Query->table, $Query->data);
			$Query->where = $this->createSQL_Where($Query->data);
		}
		$QueryStr = 'SELECT '.$Column.' FROM '.$Table.' WHERE '.$Query->where.$this->createSQL_Option($Query);
		return $this->execQuery($QueryStr, $Query->data);
	}

	/**
	 * INSERTクエリを発行する
	 * 
	 * @param string $Table 参照するテーブル
	 * @param string $Data プレイスホルダにバインドする配列
	 * @param string $NoID IDの自動設定を行わない
	 * @return int インサートしたID($NoIDがtrueの時は常に0)
	 * @access public
	 */
	public function Insert ($Query) {
		if ($Query->autovalidate) {
			$Query->data = $this->validateData($Query->table, $Query->data);
			$Table = $this->validateTable($Query->table);
		} else {
			$Table = $Query->table;
		}

		$Columns = '';
		$Values = '';
		foreach ($Query->data as $key=>$val) {
			if (!empty($Columns)) $Columns .= ', ';
			$Columns .= '`'.$key.'`';

			if (!empty($Values)) $Values .= ', ';
			$Values .= ':'.$key;
		}

		$QueryStr = 'INSERT INTO '. $Table. ' ('.$Columns.') VALUES ('.$Values.')';
		
		$this->execQuery($QueryStr, $Query->data);
		return $this->PDO->lastInsertId();
	}

	/**
	 * UPDATEクエリを発行する
	 * 
	 * 引数$Optionsで動作の設定ができます。
	 * キー名	:	解説
	 * limit	:	LIMITの値
	 * offset	:	OFFSETの値
	 * 
	 * @param string $Table 参照するテーブル
	 * @param string $WhereQuery WHERE文。
	 * @param string $Data プレイスホルダにバインドする配列
	 * @return mixed PDOStatementが返す戻り値
	 * @access public
	 */
	public function Update ($Query) {
		if ($Query->autovalidate) {
			$Query->data = $this->validateData($Query->table, $Query->data);
			$Table = $this->validateTable($Query->table);
		} else {
			$Table = $Query->table;
		}

		$SetQuery = $Query->set;
		foreach ($Query->data as $key=>$val) {
			// WHERE句、SET句に使われている変数はSETしない
			if ($Query->where && preg_match('/:'.$key. '[ ,]+/', $Query->where.' ')) {
				 continue;
			}
			if ($Query->set && preg_match('/[`\']*'.$key. '[`\']*[ =]+[^:]+/', $Query->set)) {
				 unset($Query->data[$key]); continue;
			}
		
			if (!empty($SetQuery)) $SetQuery .= ', ';
			$SetQuery .= '`'.$key.'`=:'.$key;
		}

		if (empty($Query->where)) {
			$Query->where = '1';
		}
		$QueryStr = 'UPDATE '. $Table. ' SET '. $SetQuery. ' WHERE '. $Query->where.$this->createSQL_Option($Query);

		$this->execQuery($QueryStr, $Query->data);
		return $this->PDOStatement->rowCount();
	}

	/**
	 * DELETEクエリを発行する
	 * 
	 * 引数$Optionsで動作の設定ができます。
	 * キー名	:	解説
	 * limit	:	LIMITの値
	 * offset	:	OFFSETの値
	 * 
	 * @param string $Table 参照するテーブル
	 * @param string $WhereQuery WHERE文。
	 * @param string $Data プレイスホルダにバインドする配列
	 * @return mixed PDOStatementが返す戻り値
	 * @access public
	 */
	public function Delete ($Query) {
		if ($Query->autovalidate) {
			$Query->data = $this->validateData($Query->table, $Query->data);
			$Table = $this->validateTable($Query->table);
		} else {
			$Table = $Query->table;
		}

		if (empty($Query->where)) {
			$Query->where = $this->createSQL_Where($Query->data);
		}

		$QueryStr = 'DELETE FROM '. $Table. ' WHERE '. $Query->where.$this->createSQL_Option($Query);
		return $this->execQuery($QueryStr, $Query->data);
	}

	/**
	 * テーブルを新規に作成する
	 * dropにtrueを指定すると既存のテーブルを破棄してから作成する。
	 */
	public function create_table ($Query) {
		$Data = $Query->data;
		$Table = '`'.$Query->table.'`';

		if ($Query->drop) {
			$SQL = 'DROP TABLE IF EXISTS '.$Table;
			$this->execQuery($SQL, array());
		}

		$SQL = 'CREATE TABLE IF NOT EXISTS '.$Table. ' (';
		foreach ($Data['column'] as $Name=>$Args) {
			// 名前と型
			$SQL .= '`'.$Name.'` '.$Args['type'];

			// データ長
			if (isset($Args['length'])) {
				$SQL .= '('.(int)$Args['length'].') ';
			}

			// 符号
			if (isset($Args['unsigned']) && $Args['unsigned']) {
				$SQL .= ' UNSIGNED';
			}

			// NULL
			if (isset($Args['null'])) {
				if ($Args['null']) {
				} else {
					$SQL .= ' NOT NULL';
				}
			}
			
			// DEFAULT
			if (isset($Args['default'])) {
				$SQL .= ' DEFAULT "'.$Args['default'].'"';
			}
			
			// auto_increment
			if (isset($Args['autoincrement'])) {
				$SQL .= ' AUTO_INCREMENT';
			}
			
			// コメント
			if (isset($Args['comment'])) {
				$SQL .= ' COMMENT "'.$Args['comment'].'"';
			}

			$SQL .= ', ';
		}
		$SQL = rtrim ($SQL, ', ');
		if (isset($Data['primary'])) {
			$SQL .= ', PRIMARY KEY (`'.$Data['primary'].'`)';
		}
		$SQL .= ' )';
		if (isset($Data['engine'])) {
			$SQL .= ' ENGINE='.$Data['engine'];
		}
		if (isset($Data['charset'])) {
			$SQL .= ' CHARSET='.$Data['charset'];
		}

		if (isset($Data['comment'])) {
			$SQL .= ' COMMENT="'.$Data['comment'].'"';
		}

		$SQL .= ';';
		$this->execQuery($SQL, array());
	}
}

/**
 * クエリ発行用クラス
 */
class ConnectDB_MySQL_Query {
	public $query;
	public $table; 
	public $where;
	public $set;
	public $data;
	public $column;
	public $limit;
	public $offset;
	public $group;
	public $order;
	public $drop;
	public $autovalidate;

	function __construct ($Query, $Data = array()) {
			$this->query=null;
			$this->table=null;
			$this->set=null;
			$this->where=null;
			$this->data=(array)$Data;
			$this->column='*';
			$this->limit=0;
			$this->offset=0;
			$this->group=null;
			$this->order=null;
			$this->drop=false;
			$this->autovalidate=Y('App.Helper.ConnectDB.AutoValidate');
			
			foreach ((array)$Query as $key=>$val) {
				$this->$key = $val;
			}
	}
}

// }}}
?>